import * as fs from "fs";
import { BridgeDirection } from "../core/ChainProviderFactory";

export type AttestationMetric = {
    type: "claim" | "create_account";
    direction: BridgeDirection;
    startTime: number;
    startBlock: number;
    endTime: number;
    endBlock: number;
};

type BenchmarkInfo = {
    startTime: number;
    endTime?: number;
    claimAccounts: number;
    claimIterations: number;
    createAccounts: number;
    createIterations: number;
};

type BenchmarkAggregationResult = {
    all: BenchmarkTypeResult;
    [BridgeDirection.XRP_TO_EVM]: BenchmarkTypeResult;
    [BridgeDirection.EVM_TO_XRP]: BenchmarkTypeResult;
};

type BenchmarkResult = {
    general: BenchmarkAggregationResult;
    claim: BenchmarkAggregationResult;
    createAccount: BenchmarkAggregationResult;
};

type BenchmarkTypeResult = {
    medianTime: number;
    medianBlocks: number;
    maxTime: number;
    maxBlocks: number;
    minTime: number;
    minBlocks: number;
};

export class Benchmark {
    info: BenchmarkInfo;
    metrics: AttestationMetric[] = [];

    constructor(claimAccounts = 0, claimIterations = 0, createAccounts = 0, createIterations = 0) {
        this.info = {
            startTime: new Date().getTime(),
            claimAccounts,
            claimIterations,
            createAccounts,
            createIterations,
        };
    }

    addMetric(metric: AttestationMetric) {
        this.metrics.push(metric);
        this.write();
    }
    finish() {
        this.info.endTime = new Date().getTime();
        this.write();
        this.print();
    }

    private calculateResult(metrics: AttestationMetric[]): BenchmarkAggregationResult {
        return {
            all: this.calculateTypeResult(metrics),
            [BridgeDirection.XRP_TO_EVM]: this.calculateTypeResult(metrics.filter((m) => m.direction === BridgeDirection.XRP_TO_EVM)),
            [BridgeDirection.EVM_TO_XRP]: this.calculateTypeResult(metrics.filter((m) => m.direction === BridgeDirection.EVM_TO_XRP)),
        };
    }

    private calculateTypeResult(metrics: AttestationMetric[]): BenchmarkTypeResult {
        const metricResult = metrics.reduce(
            (acc, metric) => {
                return {
                    medianTime: acc.medianTime + (metric.endTime - metric.startTime),
                    medianBlocks: acc.medianBlocks + (metric.endBlock - metric.startBlock),
                };
            },
            { medianTime: 0, medianBlocks: 0 },
        );
        metricResult.medianTime /= metrics.length;
        metricResult.medianBlocks /= metrics.length;

        let maxTime = 0,
            maxBlocks = 0,
            minTime = Infinity,
            minBlocks = Infinity;
        for (const metric of metrics) {
            const totalTime = metric.endTime - metric.startTime;
            const totalBlocks = metric.endBlock - metric.startBlock;
            if (totalTime > maxTime) maxTime = totalTime;
            if (totalTime < minTime) minTime = totalTime;
            if (totalBlocks > maxBlocks) maxBlocks = totalBlocks;
            if (totalBlocks < minBlocks) minBlocks = totalBlocks;
        }
        return {
            medianTime: metricResult.medianTime / 1000,
            medianBlocks: metricResult.medianBlocks,
            maxTime: maxTime / 1000,
            minTime: minTime / 1000,
            maxBlocks,
            minBlocks,
        };
    }

    private get result(): BenchmarkResult {
        const claimMetrics = this.metrics.filter((metric) => metric.type === "claim");
        const createAccountMetrics = this.metrics.filter((metric) => metric.type === "create_account");
        return {
            claim: this.calculateResult(claimMetrics),
            createAccount: this.calculateResult(createAccountMetrics),
            general: this.calculateResult(this.metrics),
        };
    }

    print() {
        const executionTime = ((this.info.endTime || 0) - this.info.startTime) / 1000;
        const nAtts = this.info.claimIterations * this.info.claimAccounts + this.info.createIterations * this.info.createAccounts;
        console.log("|----------------------------------------------------------|");
        console.log("|                      BENCHMARK INFO                      |");
        console.log("|----------------------------------------------------------|");
        console.log(`|                                                          |`);
        console.log(`| Execution time: ${n2s(executionTime)}s`);
        console.log(`| Attestations/s: ${n2s(nAtts / executionTime)}`);
        console.log(`|                                                          |`);
        this.printTypeResult("General", this.result.general.all);
        this.printTypeResult("General XRP->EVM", this.result.general[BridgeDirection.XRP_TO_EVM]);
        this.printTypeResult("General EVM->XRP", this.result.general[BridgeDirection.EVM_TO_XRP]);
        console.log("|----------------------------------------------------------|");
        console.log(`|                                                          |`);
        this.printTypeResult("Claim", this.result.claim.all);
        this.printTypeResult("Claim XRP->EVM", this.result.claim[BridgeDirection.XRP_TO_EVM]);
        this.printTypeResult("Claim EVM->XRP", this.result.claim[BridgeDirection.EVM_TO_XRP]);
        console.log("|----------------------------------------------------------|");
        console.log(`|                                                          |`);
        this.printTypeResult("CreateAccount", this.result.createAccount.all);
        this.printTypeResult("CreateAccount XRP->EVM", this.result.createAccount[BridgeDirection.XRP_TO_EVM]);
        this.printTypeResult("CreateAccount EVM->XRP", this.result.createAccount[BridgeDirection.EVM_TO_XRP]);
        console.log("|----------------------------------------------------------|");
    }

    private printTypeResult(name: string, typeResult: BenchmarkTypeResult) {
        console.log(`| ${name} median time: ${n2s(typeResult.medianTime)}s`);
        console.log(`| ${name} median blocks: ${n2s(typeResult.medianBlocks)}`);
        console.log(`| ${name} min time : ${n2s(typeResult.minTime)}s`);
        console.log(`| ${name} max time : ${n2s(typeResult.maxTime)}s`);
        console.log(`| ${name} min blocks : ${n2s(typeResult.minBlocks)}`);
        console.log(`| ${name} max blocks : ${n2s(typeResult.maxBlocks)}`);
        console.log(`|                                                          |`);
    }

    private write() {
        fs.writeFileSync(
            `./benchmarks/bridge/benchmark-${this.info.claimIterations}-${this.info.claimAccounts}-${this.info.createIterations}-${this.info.createAccounts}.json`,
            JSON.stringify({ info: this.info, metrics: this.metrics }),
        );
    }

    static load(path: string): Benchmark {
        const file = fs.readFileSync(path);
        const obj = JSON.parse(file.toString());
        const benchmark = new Benchmark();
        benchmark.info = obj.info;
        benchmark.metrics = obj.metrics;
        return benchmark;
    }
}

const n2s = (n: number) =>
    n.toLocaleString(undefined, {
        maximumFractionDigits: 2,
        minimumFractionDigits: 2,
    });
